<script lang="ts" setup>
import {defineModel, onMounted, ref} from 'vue'
import {ElMessage, UploadProps, UploadRawFile} from 'element-plus'
import {calculateHash, concurRequest, createChunks, getSize} from './simple.ts'
import Api from '@/api'
import {fileTypeObj, ISimpleProps} from '@/components/BaseSimple/type.ts'

defineOptions({
	name: 'BaseSimple'
})
const props = withDefaults(defineProps<ISimpleProps>(), {
	fileSize: 5
})

const model = defineModel<Array<any>>() // 双向绑定
const customAccept = ref<string>('.*') // 上传选择文件时候限制类型
const fileList = ref<Array<any>>([]) // 文件列表

onMounted(() => {
	fileList.value = model.value ?? []
	customAccept.value = props.accept ?? (props.fileType !== 'other' ? fileTypeObj[props.fileType] : '.*')
})

const beforeUpload = (rawFile: UploadRawFile) => {
	// 检验文件类型
	if (customAccept.value !== '.*') {
		let suffix = '.' + rawFile.name.substring(rawFile.name.lastIndexOf('.') + 1) // 文件名后缀不带
		let typeList = customAccept.value.split(', ')
		if (!typeList.includes(suffix)) {
			ElMessage.warning(`上传文件只能是${customAccept.value}格式`)
			return false
		}
	}
	// 校验文件大小
	let defaultSize = props.fileSize * 1024 * 1024
	if (rawFile.size > defaultSize) {
		ElMessage.warning('文件上传大小限制为' + getSize(defaultSize) + '，实际上传的大小为' + getSize(rawFile.size))
		return false
	}

	handleUpload(rawFile)
	return false
}

/***********simple start************/
const CHUNK_SIZE = 1024 * 1024 // 1MB 分片的大小

// 绑定上传事件
const handleUpload = async (file: UploadRawFile) => {
	let fileHash = '' // 文件hash
	if (!file) return
	// 创建文件分片
	const chunks = createChunks(file, CHUNK_SIZE)
	// 计算文件内容hash值
	await calculateHash(file).then(res => {
		fileHash = res
	})
	// 过滤列表中已有的文件
	if (fileList.value.find(item => item.id === fileHash)) {
		ElMessage.warning('选择的文件已上传, 已过滤')
		return
	}

	// 将上传的文件添加到列表中
	fileList.value.push({
		fileName: file.name, // 文件名称
		size: file.size, // 文件大小
		id: fileHash, // MD5计算
		num: 0, // 上传进度
		isError: false, // 是否出现错误
		isMerge: true // 是否合并成功, 用于控制是否显示预览按钮
	})
	// 校验文件、文件分片是否存在
	const objIndex = fileList.value.findIndex(item => item.id === fileHash)
	await verify(fileHash, file.name).then(async (res: any) => {
		if (res.code === 200) {
			if (res.data.existFile) {
				fileList.value[objIndex].num = 100
				fileList.value[objIndex].isMerge = false
				model.value = fileList.value
			} else {
				await uploadChunks(chunks, res.data.existChunks, file.name, fileHash)
			}
		} else {
			fileList.value[objIndex].isError = true
		}
	}).catch(() => {
		fileList.value[objIndex].isError = true
	})
}

// 校验文件、文件分片是否存在
const verify = (fileHash: string, fileName: string) => {
	return Api.verify({fileHash, fileName})
}

// 上传文件分片
const uploadChunks = async (chunks: Array<Blob>, existChunks: Array<string>, fileName: string, fileHash: string) => {
	const formDatas = chunks
		.map((chunk, index) => ({
			fileHash: fileHash,
			chunkHash: fileHash + '-' + index,
			chunk
		}))
		.filter((item) => !existChunks.includes(item.chunkHash))
		.map((item) => {
			const formData = new FormData()
			formData.append('fileHash', item.fileHash)
			formData.append('chunkHash', item.chunkHash)
			formData.append('chunk', item.chunk)
			return formData
		})
	const taskPool = formDatas.map((formData) => () => Api.upload(formData))
	// 控制请求并发
	concurRequest(taskPool, 5, (result: any, index: number) => {
		const objIndex = fileList.value.findIndex(item => item.id === fileHash)
		if (result.code === 200) {
			fileList.value[objIndex].isError = false
			fileList.value[objIndex].num = Number(((index / taskPool.length) * 100).toFixed(2))
		} else {
			fileList.value[objIndex].isError = true
		}
	}).then(() => {
		// 上传完成了开始合并
		mergeRequest(fileName, fileHash)
	}).catch(() => {
		ElMessage.error('上传出错了')
	})
}

// 合并分片请求
const mergeRequest = (fileName: string, fileHash: string) => {
	const objIndex = fileList.value.findIndex(item => item.id === fileHash)
	Api.merge({fileHash: fileHash, fileName: fileName}).then((res: any) => {
		if (res.code === 200) {
			fileList.value[objIndex].isMerge = false
			fileList.value[objIndex].url = res.data.filePath
			model.value = fileList.value
		} else {
			fileList.value[objIndex].isError = true
		}
	}).catch((err: any) => {
		fileList.value[objIndex].isError = true
		console.log(err, '合并出错了')
	})
}
/***********simple end************/

// 超出限制时执行的
const handleExceed: UploadProps['onExceed'] = (files, uploadFiles) => {
	ElMessage.warning(`限制是${props.limit}个, 你选择了${files.length} 个文件, 加起来一共是${files.length + uploadFiles.length} 个文件`)
}

// 删除列表中的文件
const remove = (item: any) => {
	fileList.value.splice(fileList.value.findIndex((el: any) => el.id === item.id), 1)
	model.value = fileList.value
}
// 预览列表中的文件
const preview = (url: string) => {
	window.open(url, '_blank')
}
</script>

<template>
	<div style="width: 100%;">
		<el-upload
			v-model:file-list="model"
			:accept="customAccept"
			:before-upload="beforeUpload"
			:limit="limit"
			:multiple="multiple"
			:on-exceed="handleExceed"
			action="http://localhost:3000/api/upload"
		>
			<el-button type="primary">选择文件</el-button>
			<template #tip>
				<div class="el-upload__tip">
					文件类型为: {{ customAccept === '.*' ? '任何文件' : customAccept }}, 并且大小为: {{ getSize(fileSize * 1024 * 1024) }}
				</div>
			</template>
		</el-upload>
		<div v-for="(item, index) in fileList" :key="index" class="relative mt10">
			<el-progress :percentage="item.num" :show-text="false" :stroke-width="50" color="#E3F8F6" stroke-linecap="butt"/>
			<div :style="{backgroundColor: item.isError ? '#fae5e5' : ''}" class="file-item fl fl-jb">
				<div>{{ item.fileName }}</div>
				<div>{{ getSize(item.size) }}</div>
				<div>
					<span v-if="item.isError" style="color: #f00;">出错了</span>
					<span v-else>{{ item.num }}%</span>
				</div>
				<div>
					<el-button v-if="!item.isMerge" link size="large" type="primary" @click="preview(item.url)">预览</el-button>
					<el-button icon="Close" link size="large" type="danger" @click="remove(item)"/>
				</div>
			</div>
		</div>
	</div>
</template>

<style lang="scss" scoped>
:deep(.el-upload-list) {
	display: none;
}

:deep(.el-progress-bar__outer), :deep(.el-progress-bar__inner) {
	border-radius: 0;
}

.file-item {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	line-height: 50px;
	padding: 0 20px;
	box-sizing: border-box;

	div {
		width: 25%;
		text-align: center;
	}

	:first-child {
		text-align: left;
	}

	:last-child {
		text-align: right;
	}
}
</style>
